查看原文
其他

从零搭建Spring Boot脚手架(6):整合Redis作为缓存

码农小胖哥 码农小胖哥 2021-05-27


1. 前言

上一文我们整合了Mybatis Plus,今天我们会把缓存也集成进来。缓存是一个系统应用必备的一种功能,除了在减轻数据库的压力之外。还在存储一些短时效的数据场景中发挥着重大作用,比如存储用户Token、短信验证码等等,目前缓存的选型还是比较多的,EHCACHEHAZELCASTCAFFEINECOUCHBASE以及本文要整合的REDIS。接下来我们将会在kono脚手架项目中集成Spring Cache以及Redis

Gitee: https://gitee.com/felord/kono day05 分支

GitHub: https://github.com/NotFound403/kono day05 分支

2. 整合目标

使项目具有缓存功能,同时将默认的JDK序列化修改为Jackson序列化以存储一些对象,同时实现一些特定的个性化的缓存空间以满足不同场景下的不同缓存TTL时间需求。

3. 依赖集成

目前只需要引入下面的依赖即可:

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

默认情况下spring-data-redis使用高性能的lettuce客户端实现,当然你可以替换为老旧的jedis

4. 缓存及 Redis 配置

缓存以及Redis相关的配置项分别为spring.cachespring.redis开头的配置,这里比较简单的配置为:

spring:
  redis:
    host: localhost
    port: 6379
  cache:
#   type: REDIS
    redis:
    # 全局过期时间
      time-to-live: 120

5. RedisTemplate 个性化

默认情况下会有两个模板类被注入Spring IoC供我们使用,需要个性化配置来满足实际的开发。

一个是RedisTemplate<Object, Object>,主要用于对象缓存,其默认使用JDK序列化,我们需要更改其序列化方式解决一些问题,比如Java 8日期问题、JSON序列化问题。需要我们重写一下。

/**
 * Redis的一些自定义配置.
 *
 * @author felord.cn
 * @since 2020 /8/17 20:39
 */

@ConditionalOnClass(ObjectMapper.class)
@Configuration(proxyBeanMethods = false)
public class RedisConfiguration {
    /**
     * Redis template redis template.
     *
     * @param redisConnectionFactory the redis connection factory
     * @return the redis template
     */

    @Bean("redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = initJacksonSerializer();
        // 设置value的序列化规则和 key的序列化规则
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 处理redis序列化问题
     * @return Jackson2JsonRedisSerializer
     */

    private Jackson2JsonRedisSerializer<Object> initJacksonSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //以下替代旧版本 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        //bugFix Jackson2反序列化数据处理LocalDateTime类型时出错
        om.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
        // java8 时间支持
        om.registerModule(new JavaTimeModule());
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }

}

另一个是StringRedisTemplate,主要处理键值都是字符串的缓存,采用默认就好。

6. 缓存个性化

使用Spring Cache做缓存的时候,有针对不同的key设置不同过期时间的场景。比如Jwt Token我想设置为一周过期,而验证码我想设置为五分钟过期。这个怎么实现呢?需要我们个性化配置RedisCacheManager。首先我通过枚举来定义这些缓存及其TTL时间。例如:

/**
 * 缓存定义枚举
 *
 * @author felord.cn
 * @see cn.felord.kono.configuration.CacheConfiguration
 * @since 2020/8/17 21:40
 */


public enum CacheEnum {

    /**
     * 用户jwt token 缓存空间 ttl 7天
     */

    JWT_TOKEN_CACHE("usrTkn"7 * 24 * 60 * 60),
    /**
     * 验证码缓存 5分钟ttl
     */

    SMS_CAPTCHA_CACHE("smsCode"5 * 60);

    /**
     * 缓存名称
     */

    private final String cacheName;
    /**
     * 缓存过期秒数
     */

    private final int ttlSecond;

    CacheEnum(String cacheName, int ttlSecond) {
        this.cacheName = cacheName;
        this.ttlSecond = ttlSecond;
    }

    public String cacheName() {
        return this.cacheName;
    }


    public int ttlSecond() {
        return this.ttlSecond;
    }
}

这样就能很清楚地描述个性化的缓存了。

然后我们通过向Spring IoC分别注入RedisCacheConfigurationRedisCacheManagerBuilderCustomizer来个性化配置,你可以留意CacheEnum是如何工作的。如果你有其它的个性化需要也可以对这两个配置类进行定制化。

import cn.felord.kono.enumeration.CacheEnum;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;
import java.util.EnumSet;
import java.util.stream.Collectors;

/**
 * redis 缓存配置.
 *
 * @author felord.cn
 * @since 2020 /8/17 20:14
 */

@EnableCaching
@Configuration
public class CacheConfiguration {


    /**
     * Redis cache configuration.
     *
     * @param redisTemplate the redis template
     * @return the redis cache configuration
     */

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(RedisTemplate<Object, Object> redisTemplate, CacheProperties cacheProperties) {
         // 参见 spring.cache.redis
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // 缓存的序列化问题
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(redisTemplate.getValueSerializer()));

        if (redisProperties.getTimeToLive() != null) {
            // 全局 TTL 时间
            redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            // key 前缀值
            redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            // 默认缓存null值 可以防止缓存穿透
            redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            // 不使用key前缀
            redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
        }
        return redisCacheConfiguration;
    }


    /**
     * Redis cache manager 个性化配置缓存过期时间.
     * @see RedisCacheManager,CacheEnum
     * @return the redis cache manager builder customizer
     */

    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(RedisCacheConfiguration redisCacheConfiguration) {

        return builder -> builder.cacheDefaults(redisCacheConfiguration)
                // 自定义的一些缓存配置初始化 主要是特定缓存及其ttl时间
                .withInitialCacheConfigurations(EnumSet.allOf(CacheEnum.class).stream()
                        .collect(Collectors.toMap(CacheEnum::cacheName,
                                cacheEnum -> redisCacheConfiguration.entryTtl(Duration.ofSeconds(cacheEnum.ttlSecond())))));
    }

}

个性化的同时我们可以通过@EnableCaching开启Spring Cache缓存支持。关于Spring Cache的细节可以通过文章Spring Cache 详解来了解。

验证Spring Cache Redis缓存个性化

请注意,只有通过Spring Cache操作缓存才会达到上图的效果。命令行操作需要显式的声明指令。

7. 总结

最近事情比较多,所以难得抽出时间来搞一搞。如果你在实际开发中遇到需要整合的功能也可以告诉我,同时如果你发现整合中的一些缺陷或者 Bug 请提交 ISSUE。多多关注:码农小胖哥,跟我一起整合开发脚手架。

往期推荐:

想投递大厂,但还没有项目经验?| 100个免费名额,先到先得

2020-08-18

Docker真的被禁止使用了?

2020-08-17

Java SPI 居然这么多知名框架在用

2020-08-13


如果你觉得本文有用,请大力的点赞、转发、再看。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存